package com.robertux.leaderboard.process;

import java.math.BigDecimal;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Properties;

import javax.persistence.EntityManager;
import javax.persistence.Persistence;

import org.apache.log4j.Logger;

import com.robertux.leaderboard.beans.EstadoEntidad;
import com.robertux.leaderboard.beans.ModalidadGrupoProducto;
import com.robertux.leaderboard.beans.TipoMeta;
import com.robertux.leaderboard.beans.entities.AsistenciaUsuario;
import com.robertux.leaderboard.beans.entities.DetalleGrupoProducto;
import com.robertux.leaderboard.beans.entities.DetalleProgresoMeta;
import com.robertux.leaderboard.beans.entities.ExclusionCliente;
import com.robertux.leaderboard.beans.entities.GrupoProducto;
import com.robertux.leaderboard.beans.entities.Meta;
import com.robertux.leaderboard.beans.entities.MetaProducto;
import com.robertux.leaderboard.beans.entities.ProgresoMeta;
import com.robertux.leaderboard.beans.entities.PuntosAcumulado;
import com.robertux.leaderboard.beans.entities.PuntosVenta;
import com.robertux.leaderboard.beans.entities.Usuario;
import com.robertux.leaderboard.beans.entities.external.ItemVenta;
import com.robertux.leaderboard.util.gestoresModelos.GestorEntidades;
import com.robertux.leaderboard.util.gestoresModelos.GestorEntidadesFarmacia;
import com.robertux.leaderboard.util.gestoresModelos.GestorExclusiones;
import com.robertux.leaderboard.util.gestoresModelos.GestorMetas;
import com.robertux.leaderboard.util.gestoresModelos.GestorProgresos;
import com.robertux.leaderboard.util.gestoresModelos.GestorPuntosAcumulados;
import com.robertux.leaderboard.util.gestoresModelos.GestorPuntosVentas;

public class MetasUpdater implements Runnable {
	public static final String LEADERBOARDPUPD_PU = "LeaderboardUpdPU";
	
	private Logger logger;
	private DateFormat dFmt;
	private GestorProgresos gProgresos;
	private GestorEntidadesFarmacia gEntFrm;
	private GestorExclusiones gExcls;
	private GestorPuntosAcumulados gPuntosAcumulados;
	private GestorPuntosVentas gPuntosVentas;
	private EntityManager eMgr;
	private GestorMetas gMetas;
	private long idMeta;
	private Meta meta;
	private Properties sqlServerConnProps, mysqlConnProps;
	private int index, total;
	private Calendar calHoy;
	private Calendar calActualizacion;
	
	public MetasUpdater(Calendar calActualizacion, Properties sqlServerConnProps, Properties mysqlConnProps, long idMeta, int index, int total) {
		this.logger = Logger.getLogger(this.getClass());
		this.dFmt = new SimpleDateFormat("MM/yyyy");
		this.sqlServerConnProps = sqlServerConnProps;
		this.mysqlConnProps = mysqlConnProps;
		this.idMeta = idMeta;
		this.index = index;
		this.total = total;
		this.calHoy = new GregorianCalendar();
		this.calActualizacion = calActualizacion;
	}
	
	@Override
	public void run() {
		try {
			this.eMgr = Persistence.createEntityManagerFactory(LEADERBOARDPUPD_PU).createEntityManager();
			
			this.gEntFrm = new GestorEntidadesFarmacia(this.sqlServerConnProps);
			this.gMetas = new GestorMetas(new GestorEntidades<Meta>(this.eMgr));
			this.gProgresos = new GestorProgresos(this.eMgr, this.mysqlConnProps);
			this.gExcls = new GestorExclusiones(new GestorEntidades<ExclusionCliente>(this.eMgr));
			this.gPuntosAcumulados = new GestorPuntosAcumulados(new GestorEntidades<PuntosAcumulado>(this.eMgr));
			this.gPuntosVentas = new GestorPuntosVentas(new GestorEntidades<PuntosVenta>(this.eMgr));
		
			this.meta = this.gMetas.getMeta(this.idMeta);
			this.updateProgresoMeta();
		} catch (Exception e) {
			this.logger.error("Error tratando de configurar las conexiones a las bases de datos: " + e.getMessage(), e);
		} finally {
			if(this.eMgr != null) {
				try { this.eMgr.close(); this.eMgr = null; } catch(Exception ex) { this.logger.warn("Error tratando de cerrar el EntityManager: " + ex.getMessage()); }
			}
			if(this.gEntFrm != null) {
				this.gEntFrm.closeConnection();
			}
			if(this.gProgresos != null) {
				this.gProgresos.closeConnection();
			}
		}
	}

	public void updateProgresoMeta() {
		this.logger.info("\n\n\n==== Actualizando meta " + (this.index+1) + " de " + this.total + " con ID " + this.meta.getId() + ", " + this.meta.getNombre() + "(>=" + (this.meta.getTipo()==1? "$": "") + this.meta.getValor() + (this.meta.getTipo()==2? " unidades": "") + ")");
		this.logger.info("==== Meta asignada a usuario " + this.meta.getUsuario().getVendedorId() + " - " + this.meta.getUsuario().getNombre() + " en Sucursal " + this.meta.getSucursalId() + " con ponderación de " + this.meta.getPonderacion() + "%");
		
		ProgresoMeta progreso = getMonthProgreso(this.calActualizacion);
		for(DetalleProgresoMeta dProg: progreso.getDetalleProgresosMeta()) {
			this.gProgresos.getEntityManager().remove(this.gProgresos.getEntityManager().merge(dProg));
		}
		progreso.getDetalleProgresosMeta().clear();
		this.gProgresos.actualizarProgreso(progreso);
		
		List<ExclusionCliente> exclusiones = this.gExcls.getExclusiones();
		TipoMeta tipoMeta = TipoMeta.fromCodigo(this.meta.getTipo());
		double totalVentas = 0;
		
		for(MetaProducto mPrd: this.meta.getMetaProductos()) {
			double ventasMeta = 0;
			
			GrupoProducto gPrd = mPrd.getGrupoProducto();
			ventasMeta = calcularVentasMeta(gPrd, exclusiones, tipoMeta);
			totalVentas += ventasMeta;
			
			DetalleProgresoMeta detalleProgreso = new DetalleProgresoMeta(gPrd, progreso, new BigDecimal(ventasMeta)); 
			this.logger.debug("Agregando detalleProgreso: " + detalleProgreso);
			progreso.addDetalleProgresoMeta(detalleProgreso);
			
			this.actualizarPuntosVenta(this.calActualizacion, ventasMeta, gPrd, exclusiones);
		}
		
		int totalDiasMes = this.calActualizacion.getActualMaximum(Calendar.DAY_OF_MONTH);
		int totalDiasAsistencia = this.calActualizacion.getActualMaximum(Calendar.DAY_OF_MONTH);
		
		AsistenciaUsuario asistencia = this.getAsistencia(meta.getUsuario(), this.calActualizacion);
		if(asistencia != null) {
			totalDiasAsistencia = asistencia.getDiasAsistencia();
		}
		double asistenciaProporcional = (((double)totalDiasAsistencia) / totalDiasMes);
		
		double progresoPreliminar = (totalVentas / this.meta.getValor().doubleValue()) * 100;
		double progresoProporcional = progresoPreliminar * asistenciaProporcional;
		
		this.logger.info("Total ventas meta: " + totalVentas + ". Total esperado: " + this.meta.getValor());
		this.logger.info("Asistencia proporcional: " + asistenciaProporcional + ". Progreso preliminar: " + progresoPreliminar + ". Progreso proporcional: " + progresoProporcional);
		
		progreso.setVentas(new BigDecimal(totalVentas));
		progreso.setValor(new BigDecimal(progresoProporcional));
		this.logger.info("Nuevo valor para progreso con ID " + progreso.getId() + ": " + progreso.getValor());
		
		boolean progresoActualizado = this.gProgresos.actualizarProgreso(progreso);
		this.logger.info("Progreso con ID " + progreso.getId() + " actualizado? " + progresoActualizado);
	}

	protected double calcularVentasMeta(GrupoProducto gPrd, List<ExclusionCliente> exclusiones, TipoMeta tipoMeta) {
		List<ItemVenta> itemsVenta = null;
		double ventasMeta = 0;
		
		ModalidadGrupoProducto modalidad = ModalidadGrupoProducto.fromCodigo(gPrd.getModalidad());
		//Si la modalidad es TODOS pero solo hay un producto en el grupo entonces no vale la pena que ingrese aquí dado que se puede considerar como modalidad CUALQUIERA
		if(modalidad.equals(ModalidadGrupoProducto.TODOS) && gPrd.getDetalleGrupoProductos().size() > 1) {
			//Se obtiene la suma de cantidades y montos de productos vendidos por este empleado en su sucursal asignada en el mes recibido como parametro
			//Cada factura debe contener TODOS los productos del grupo de productos actual para que sea seleccionada
			itemsVenta = this.gEntFrm.getItemsVentasProducto(this.calActualizacion, this.meta, gPrd, exclusiones, true);
			this.filtrarDevoluciones(itemsVenta);
			this.logger.debug("Items filtrados sin devoluciones: " + Arrays.toString(itemsVenta.toArray()));
			
			long idFacturaAnterior = 0, numFacturaAnterior = 0;
			for(ItemVenta itemVenta: itemsVenta) {
				this.logger.debug("idFacturaAnterior: " + idFacturaAnterior);
				if((itemVenta.getIdFactura() == idFacturaAnterior && itemVenta.getNumeroFactura() == numFacturaAnterior) || itemVenta.getCantidadTotalEnVenta() < 0) {
					//Como por factura solamente se estan seleccionando los productos del grupo de productos requerido, no se tienen que sumar todos los productos sino
					//solamente la fraccion de uno de ellos con respecto a la cantidad de unidades requeridas de este producto segun el grupo de productos.
					//this.logger.debug("--Saltando itemVenta: " + itemVenta);
					continue;
				}
				idFacturaAnterior = itemVenta.getIdFactura();
				numFacturaAnterior = itemVenta.getNumeroFactura();
				//Se busca el producto segun el ArticuloId de la consulta SQL
				int index = gPrd.getDetalleGrupoProductos().indexOf(new DetalleGrupoProducto(itemVenta.getCodigoProducto(), 0));
				//Si el producto se encuentra, se extrae la cantidad de este producto requerida para cumplir con la meta...
				if(index >= 0 && Math.abs(itemVenta.getCantidadTotalEnVenta()) >= gPrd.getDetalleGrupoProductos().get(index).getCantidad()) {
					//this.logger.debug("--Agregando itemVenta: " + itemVenta);
					double ventasCalculadas = 0;
					//Si el tipo de meta es por montos, solamente se suma el monto total de la factura para este producto
					ventasCalculadas = (tipoMeta == TipoMeta.POR_MONTOS_VENTAS? 
							itemVenta.getPrecioTotalEnVenta(): 
							//Si el tipo de meta es por cantidades y el grupo de productos tiene una cantidad minima de este producto para cumplir con la meta, se extrae la fraccion
							Math.floor(itemVenta.getCantidadTotalEnVenta() / gPrd.getDetalleGrupoProductos().get(index).getCantidad()));
						
					//this.logger.debug("ventasCalculadas: " + ventasCalculadas);
					ventasMeta += ventasCalculadas;
				}					
			}
				
			//TODO: Falta ir guardando por meta una lista de los ItemVenta que, a pesar que ya fueron calculados, 
			//guardar las cantidades que ya fueron utilizadas para calculos anteriores, de manera que si algun grupo producto posterior
			//tiene asociado el mismo producto ID que estaba previamente guardado en la lista de ItemsVenta ya ocupados, que a este nuevo
			//producto resultado del calculo se le resten las cantidades ya calculadas y que nuevamente se evalue si la diferencia aun cumple con
			//la minima cantidad exigida por el grupo de producto actual
		}
		else {  //modalidad es CUALQUIERA o modalidad es TODOS pero solo hay un producto en el grupo
			
			//Se obtiene la suma de cantidades y montos de productos vendidos por este empleado en su sucursal asignada en el mes recibido como parametro
			//Cada factura debe contener al menos uno (CUALQUIERA) de los productos del grupo de productos actual para que sea seleccionada
			itemsVenta = this.gEntFrm.getItemsVentasProducto(this.calActualizacion, this.meta, gPrd, exclusiones);
			//this.filtrarDevoluciones(itemsVenta);
			//this.logger.debug("Items filtrados sin devoluciones: " + Arrays.toString(itemsVenta.toArray()));
			
			for(ItemVenta itemVenta: itemsVenta) {
				//Se busca el producto segun el ArticuloId de la consulta SQL
				int index = gPrd.getDetalleGrupoProductos().indexOf(new DetalleGrupoProducto(itemVenta.getCodigoProducto(), 0));
				//Si el producto se encuentra, se extrae la cantidad de este producto requerida para cumplir con la meta...
				if(index >= 0 && Math.abs(itemVenta.getCantidadTotalEnVenta()) >= gPrd.getDetalleGrupoProductos().get(index).getCantidad()) {
					double ventasCalculadas = 0;
					//Si el tipo de meta es por montos, solamente se suma el monto total de la factura para este producto
					ventasCalculadas = (tipoMeta == TipoMeta.POR_MONTOS_VENTAS? 
							itemVenta.getPrecioTotalEnVenta(): 
							//Si el tipo de meta es por cantidades y el grupo de productos tiene una cantidad minima de este producto para cumplir con la meta, se extrae la fraccion
							Math.floor(itemVenta.getCantidadTotalEnVenta() / gPrd.getDetalleGrupoProductos().get(index).getCantidad()));
						
					ventasMeta += ventasCalculadas;
				}
			}
		}
		
		this.logger.debug("Total ventas meta para grupo de productos " + gPrd + ": " + ventasMeta);
		return ventasMeta;
	}
	
	protected void filtrarDevoluciones(List<ItemVenta> itemsVenta) {
		List<ItemVenta> itemsRemover = new ArrayList<ItemVenta>();
		int cantidadDevolucion = 0;
		
		for(ItemVenta item: itemsVenta) {
			if((cantidadDevolucion = item.getCantidadTotalEnVenta()) < 0) {
				itemsRemover.add(item);
				
				while(cantidadDevolucion < 0) {
					for(ItemVenta itemDevolucion: itemsVenta) {
						//this.logger.debug("\t\t\t--- item: " + itemDevolucion + " cantidadDevolucion: " + cantidadDevolucion);
						if(itemDevolucion.getCantidadTotalEnVenta() > 0 && itemDevolucion.getCodigoProducto().equals(item.getCodigoProducto()) 
								&& ((cantidadDevolucion + itemDevolucion.getCantidadTotalEnVenta()) <= 0)) {
							itemsRemover.add(itemDevolucion);
							cantidadDevolucion += itemDevolucion.getCantidadTotalEnVenta(); 
							if(cantidadDevolucion == 0) break;
						}
					}
				}
			}
		}
		
		for(ItemVenta itemRemover: itemsRemover) {
			//this.logger.debug("\t\t-- Removiendo item " + itemRemover);
			itemsVenta.remove(itemRemover);
		}
	}
	
	protected void actualizarPuntosVenta(Calendar calEjecucion, double ventasUnidades, GrupoProducto gPrd, List<ExclusionCliente> exclusiones) {
		try {
			//Obtiene calculo actual de puntos acumulados por usuario o sino crea una nueva instancia
			PuntosAcumulado puntos = this.gPuntosAcumulados.getPuntos(this.meta.getUsuario().getId(), gPrd.getId(), calEjecucion.get(Calendar.YEAR), calEjecucion.get(Calendar.MONTH)+1);
			puntos = (puntos != null? puntos: new PuntosAcumulado(calEjecucion.getTime(), 0, this.meta.getUsuario(), gPrd));
			
			//Obtiene el factor de puntos por venta para el grupo de producto actual y se lo asigna a la instancia de puntos, 
			//o le asigna cero en caso que no exista el factor de puntos por venta
			PuntosVenta pVenta = this.gPuntosVentas.getPuntosPorVentas(gPrd.getId());
			puntos.setPuntos(pVenta != null? pVenta.getPuntos() * new Double(ventasUnidades).intValue(): 0);
			
			boolean puntosActualizados = this.gPuntosAcumulados.actualizarPuntos(puntos);
			this.logger.info("Resultado de actualización de PuntosAcumulados " + puntos + ": " + puntosActualizados);
		} catch (Exception e) {
			this.logger.error("Error calculando puntos para usaurio/grupo productos y actualizando en la base de datos: " + e.getMessage());
		}
	}

	protected ProgresoMeta getMonthProgreso(Calendar calEjecucion) {
		ProgresoMeta progreso = this.gMetas.getProgreso(this.meta, calEjecucion.get(Calendar.MONTH)+1, calEjecucion.get(Calendar.YEAR));
		Calendar calProgreso = new GregorianCalendar();
		if(progreso != null) {
			calProgreso.setTime(progreso.getFecha());
		}
		
		if(progreso == null || (calProgreso.get(Calendar.MONTH) != calEjecucion.get(Calendar.MONTH) || calProgreso.get(Calendar.YEAR) != calEjecucion.get(Calendar.YEAR))) {
			progreso = new ProgresoMeta();
			progreso.setEstado(EstadoEntidad.ACTIVO.getCodigo());
			progreso.setFecha(calEjecucion.getTime());
			progreso.setValor(BigDecimal.ZERO);
			progreso.setVentas(BigDecimal.ZERO);
			progreso.setUltimaActualizacion(new Date());
			this.logger.info("Creando nuevo progreso para el mes " + dFmt.format(progreso.getFecha()));
			progreso.setMeta(this.meta);
			progreso = this.gProgresos.actualizar(progreso);
		} else {
			this.logger.info("Actualizando progreso con ID " + progreso.getId());
			progreso.setUltimaActualizacion(new Date());
		}
		return progreso;
	}
	
	protected AsistenciaUsuario getAsistencia(Usuario us, Calendar calEjecucion) {
		if(us != null && us.getAsistencias() != null) {
			for(AsistenciaUsuario asistencia: us.getAsistencias()) {
				if(asistencia != null && asistencia.getAnio() == calEjecucion.get(Calendar.YEAR) && asistencia.getMes() == (calEjecucion.get(Calendar.MONTH)+1)) {
					return asistencia;
				}
			}
		}
		
		return null;
	}
}
